#!/usr/bin/env python
# -*- coding: utf-8 -*-

import log

from help import bisectLeft,bisectRight
from key import Key
from scan import ScanContext,ScanSpec,RowInterval,TimeInterval

class   ScanCollisionError(Exception) : pass
class   ScanSpecError(Exception) : pass

#######################################################################

class   CellStore(object) :
    SEPARATOR_KEY_VALUE = '\t'
    TAG_MARK_BEGIN = 'CellTagBegin'
    TAG_MARK_END   = 'CellTagEnd'
    SIZE_OF_BLOCK_TO_INDEX = 65536

    # TAG_STRING ::= BEGIN_MARK RECORD_DATA_END RECORD_INDEX_END BLOCK_INDEX_END END_MARK
    TAG_STRING_LENGTH = 12 + 1 + 16 + 1 + 16 + 1 + 16 + 1 + 10   # no trailling \n

    def __init__(self, filename) :
        self.filename = filename
        self.write_opened = False
        self.write_handle = None
        self.write_record_indexes = []

        self.write_block_indexes = []
        self.write_block_indexes_size = 0   # accumulate size-of-record-index to mark block index
        self.write_block_indexes_offset = 0
        self.write_block_indexes_last_key = None

        self.read_opened = False
        self.read_handle = None
        self.read_record_data_end_offset  = 0
        self.read_record_index_end_offset = 0
        self.read_block_index_end_offset  = 0
        self.read_record_indexes = {}
        self.read_block_indexes = []        # bisect

        self.__scanning = False
        self.__scanning_offset = 0
        self.__scan_begin_row = None
        self.__scan_end_row = None
        # __init__()

    def __del__(self) :
        if self.write_opened :
            self.__writeIndex()

            if self.write_handle : self.write_handle.close()
            self.write_opened = False
            self.write_buffer_index = []

        if self.read_opened :
            if self.read_handle : self.read_handle.close()
            self.read_opened = False
            self.read_buffer_index = []
        # __del__()

    def __init_write(self) :
        if self.write_opened : return

        if not self.filename :
            raise RuntimeError, 'Invalid filename "%s" for %s' % (self.filename, self.__class__.__name__)
        self.write_handle = open(self.filename, 'w')
        self.write_opened = True
        # __init_write()

    def append(self, key, value) :
        if not self.write_opened :
            self.__init_write()

        offset = self.write_handle.tell()
        record = '%s%s%s\n' % (key, self.SEPARATOR_KEY_VALUE, value)
        self.write_handle.write(record)

        index = '%s\t%d\t%d\n' % (key, offset, len(record))
        self.write_record_indexes.append(index)

        self.write_block_indexes_last_key = key
        self.write_block_indexes_size += len(index)
        if self.write_block_indexes_size >= self.SIZE_OF_BLOCK_TO_INDEX :
            block_index = (key, self.write_block_indexes_offset, self.write_block_indexes_size)
            self.write_block_indexes.append(block_index)
            self.write_block_indexes_offset += self.write_block_indexes_size
            self.write_block_indexes_size = 0
            self.write_block_indexes_last_key = None
        # append()

    def __writeIndex(self) :
        # this method should be call once and ONLY ONCE
        if not self.write_opened : return False

        record_data_end_offset = self.write_handle.tell()
        self.write_handle.writelines(self.write_record_indexes)

        record_index_end_offset = self.write_handle.tell()
        if self.write_block_indexes_last_key is not None :
            block_index = (self.write_block_indexes_last_key, self.write_block_indexes_offset, self.write_block_indexes_size)
            self.write_block_indexes.append(block_index)
            self.write_block_indexes_offset += self.write_block_indexes_size
            self.write_block_indexes_size = 0
            self.write_block_indexes_last_key = None
        #print self.write_block_indexes
        #print 'OFFSET to add:',record_data_end_offset
        block_indexes = [ '%s\t%d\t%d\n' % (x[0], x[1] + record_data_end_offset, x[2]) for x in self.write_block_indexes ]
        #print block_indexes
        self.write_handle.writelines(block_indexes)

        block_index_end_offset = self.write_handle.tell()

        tag_fields = (self.TAG_MARK_BEGIN, '%016d' % record_data_end_offset, '%016d' % record_index_end_offset, '%016d' % block_index_end_offset, self.TAG_MARK_END)
        write_buffer_tag = self.SEPARATOR_KEY_VALUE.join(tag_fields)
        self.write_handle.write(write_buffer_tag)
        return True
        # __writeIndex()

    def __init_read(self) :
        if self.read_opened : return

        if not self.filename :
            raise RuntimeError, 'Invalid filename "%s" for %s' % (self.filename, self.__class__.__name__)
        self.read_handle = open(self.filename, 'r')

        SEEK_END = 2
        self.read_handle.seek(-self.TAG_STRING_LENGTH, SEEK_END)
        read_buffer_tag = self.read_handle.read(self.TAG_STRING_LENGTH)
        #print self.read_buffer_tag
        tag_begin, record_data_end_offset, record_index_end_offset, block_index_end_offset, tag_end = read_buffer_tag.split(self.SEPARATOR_KEY_VALUE)
        self.read_record_data_end_offset  = int(record_data_end_offset)
        self.read_record_index_end_offset = int(record_index_end_offset)
        self.read_block_index_end_offset  = int(block_index_end_offset)

        ## read record index
        #self.read_handle.seek(self.read_record_data_end_offset)
        #offset = self.read_handle.tell()
        #while offset < self.read_record_index_end_offset :
        #    line = self.read_handle.readline().strip()
        #    _key, _offset, _length = line.split('\t')
        #    self.read_record_indexes[_key] = (_offset, _length)
        #    offset = self.read_handle.tell()

        ## read block Index
        self.read_handle.seek(self.read_record_index_end_offset)
        offset = self.read_handle.tell()
        while offset < self.read_block_index_end_offset :
            line = self.read_handle.readline().strip()
            _key, _offset, _length = line.split('\t')
            self.read_block_indexes.append( (_key, int(_offset), int(_length)) )
            offset = self.read_handle.tell()

        self.read_handle.seek(0)
        self.read_opened = True
        # __init_read()

    def next(self) :
        if not self.read_opened :
            self.__init_read()

        offset = self.read_handle.tell()
        if offset >= self.read_record_data_end_offset :
            raise StopIteration

        line = self.read_handle.readline().strip()
        if not line :
            raise StopIteration
        key, value = line.split(self.SEPARATOR_KEY_VALUE, 1)
        return key,value
        # next()

    def prescan(self, scan_spec) :
        '''
        prepare for scan

        raise ScanCollisionError   when another scanner is working on it
        raise StopIteration        when no records falled in this CellStore

        todo : to identify which scanner is scanning this store ??
        '''
        #log.debug('%s::prescan(spec="%s")', self.__class__.__name__, scan_spec)
        if not scan_spec :
            raise ScanSpecError, 'Empty ScanSpec'

        if self.__scanning :
            raise ScanCollisionError, 'the cellstore is scanned by another scanner now.'

        if not self.read_opened :
            self.__init_read()

        ## STEP. assemble scan_begin_row, scan_end_row
        ## STEP. locates scan_begin_row in block-index and record-index
        ##       if out-of-bound, raise StopIteration
        ##       else update self.__scan_begin_row, self.__scanning_offset
        ## STEP. locates scan_end_row in block_index and record-IndexError
        ##       if out-of-bound, self.__scan_end_row = ''
        ##       else update self.__scan_end_row
        ##
        ## NOTE: be careful INCLUSIVE criteria

        row_interval = scan_spec.row_interval
        if not row_interval : row_interval = RowInterval('\x00\x00\x00\x00', '\xFF\xFF\xFF\xFF')
        time_interval = scan_spec.time_interval
        if not time_interval : time_interval = TimeInterval()

        #log.debug('RowInterval:%s', row_interval)
        key_begin = Key(row_key=row_interval.end_row, timestamp=time_interval.time_end).key
        key_begin_inclusive = row_interval.end_row_inclusive
        key_end = Key(row_key=row_interval.begin_row, timestamp=time_interval.time_begin).key
        key_end_inclusive = row_interval.begin_row_inclusive
        self.__scan_begin_row = self.getScanBeginRow(key_begin, key_begin_inclusive)
        if not self.__scan_begin_row :
            raise StopIteration, 'Scan Begin Key "%s" out of bound' % `key_begin`
        self.__scan_end_row = self.getScanEndRow(key_end, key_end_inclusive)
        if not self.__scan_end_row :
            self.__scan_end_row = ''

        self.__scanning_offset = self.__scan_begin_row[1]

        #log.debug('SCAN [%s .. %s]', self.__scan_begin_row, self.__scan_end_row)
        #log.debug('%s::prescan() done', self.__class__.__name__)
        self.__scanning = True
        # prescan()

    def forward(self) :
        '''
        scan the CellStore, forward and return a result
        '''
        if not self.__scanning : raise StopIteration

        if self.__scan_end_row and self.__scanning_offset > self.__scan_end_row[1] :
            raise StopIteration

        if self.__scanning_offset >= self.read_record_data_end_offset :
            raise StopIteration

        if self.__scanning_offset != self.read_handle.tell() :
            self.read_handle.seek(self.__scanning_offset)

        line = self.read_handle.readline().strip()
        if not line :
            raise StopIteration
        key, value = line.split(self.SEPARATOR_KEY_VALUE, 1)

        self.__scanning_offset = self.read_handle.tell()
        return key,value
        # scan()

    def postscan(self) :
        #log.debug('%s::postscan()', self.__class__.__name__)
        self.__scanning = False
        # postscan

    def getScanBeginRow(self, key_begin, key_begin_inclusive) :
        #log.debug('ScanBeginRow Key-To-Find: %s', `key_begin`)
        #log.debug('ScanBeginRow block-indexes:%s', self.read_block_indexes)

        key_begin_index = (key_begin, 0, 0)
        index = bisectLeft(self.read_block_indexes, key_begin_index)
        #log.debug('ScanBeginRow BLOCK-INDEX  bisectLeft %s returns %d', key_begin_index, index)
        _key,_offset,_length = self.read_block_indexes[index]
        record_index_block = self.readBlockData(_offset, _length)
        record_indexes = [line.split('\t') for line in record_index_block.strip().split('\n')]
        record_indexes = [(x[0], int(x[1]), int(x[2])) for x in record_indexes]
        record_index = bisectLeft(record_indexes, key_begin_index)
        #log.debug('ScanBeginRow RECORD-INDEX bisectLeft %s returns %d', key_begin_index, record_index)
        return  record_indexes[record_index]
        # getScanBeginRow()

    def getScanEndRow(self, key_end, key_end_inclusive) :
        key_end_index = (key_end, 0, 0)
        index = bisectRight(self.read_block_indexes, key_end_index)
        #log.debug('ScanEndRow BLOCK-INDEX  bisectRight %s returns %d', key_end_index, index)
        if index >= len(self.read_block_indexes) :
            # we will scan to end
            return None

        _key,_offset,_length = self.read_block_indexes[index]
        record_index_block = self.readBlockData(_offset, _length)
        record_indexes = [line.split('\t') for line in record_index_block.strip().split('\n')]
        record_indexes = [(x[0], int(x[1]), int(x[2])) for x in record_indexes]
        record_index = bisectRight(record_indexes, key_end_index)
        #log.debug('ScanEndRow RECORD-INDEX bisectRight %s returns %d', key_end_index, record_index)
        return  record_indexes[record_index]
        # getScanEndRow()

    def readBlockData(self, offset, length) :
        self.read_handle.seek(offset)
        data = self.read_handle.read(length)

        #log.debug('Record Indexes: \n%s', data)
        return data
        # readBlockData

    # class CellStore

#######################################################################

class   CellStoreScanner(object) :
    def __init__(self, scan_context) :
        self.scan_context = scan_context
        self.store = None

        self.__scanning = False
        # __init__()

    def __del__(self) :
        #log.debug('%s::__del__() scanning=%s spec=%s', self.__class__.__name__, self.__scanning, self.scan_context.scan_spec )
        self.detach()
        # __del__()

    def __iter__(self) :
        return self
        # __iter__()

    def next(self) :
        '''
        return tuple(key, value) if there are records matching search criteria
        else, raise StopIteration

        STEP. store.prescan( scan_spec )
              to LOCK CellStore for this scan only
        STEP. key, value = store.next()
        STEP. store.postscan() after scanning
              to UNLOCK CellStore for this scan
        '''
        if not self.__scanning :
            self.store.prescan(self.scan_context.scan_spec)
            self.__scanning = True

        key,value = self.store.forward()
        #log.debug('RESULT key=%s value=%s', key, value)
        return (key,value)
        # next()

    def attach(self, store) :
        if not self.__scanning :
            self.store = store
            self.store.prescan(self.scan_context.scan_spec)
            self.__scanning = True
        # attach()

    def detach(self) :
        if self.store :
            self.store.postscan()
        self.store = None
        self.__scanning = False
        # detach()
    # class CellStoreScanner

#######################################################################

